/*
 * Class NativeUtils is published under the The MIT License:
 *
 * Copyright (c) 2012 Adam Heinrich <adam@adamh.cz>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package be.ugent.jgaborator.util;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/**
 * A simple library class which helps with loading dynamic libraries stored in the
 * JAR archive. These libraries usually contain implementation of some methods in
 * native code (using JNI - Java Native Interface).
 * 
 * @see <a href="http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar">http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar</a>
 * @see <a href="https://github.com/adamheinrich/native-utils">https://github.com/adamheinrich/native-utils</a>
 *
 */
public class ZigNativeUtils {
 
    /**
     * The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}.
     */
    private static final int MIN_PREFIX_LENGTH = 3;
    private static final String NATIVE_FOLDER_PATH_PREFIX = "zignativeutils";

    /**
     * Temporary directory which will contain the DLLs.
     */
    private static File temporaryDir;

    /**
     * Private constructor - this class will never be instanced
     */
    private ZigNativeUtils() {
    }

    /**
     * Load a library with OS and architecture detection. The idea is that libraries are found
     *
     * @param folder The folder of the library with forward slashes starting with '/' e.g. '/jni'
     * @param folder the base name of the library which is then translated to system names gabor is translated
     *               to gabor.dll on windows or libgabor.dynlib on macOS.
     * @return True if the library is loaded successfully
     */
    public static boolean loadLibraryFromJarWithOSDetection(String folder,String libraryName) {
        String systemLibraryName = System.mapLibraryName(libraryName);
        List<String> foundLibraries;

        System.err.print("Trying to find libraries in  folder " + folder + " ending in " + systemLibraryName);

        try {
            foundLibraries = listResources(folder);
        } catch (URISyntaxException | IOException e) {
            throw new RuntimeException(e);
        }

        if(foundLibraries.size() == 0 ){
            System.err.print("No native JNI libraries found in JAR archive");
        }
        for(String resourceName : foundLibraries){
            if(resourceName.contains(systemLibraryName)){

                System.err.println("Try to load JNI library " + resourceName + " from JAR archive.");
                try{
                    ZigNativeUtils.loadLibraryFromJar(resourceName);
                    System.err.println("Native library library " + resourceName + " loaded!");
                    return true;
                }catch (IOException | UnsatisfiedLinkError e){
                    System.err.println("Failed to load library " + resourceName  + " potentially attempting others.\n" + e.getMessage());
                }
            }
        }
        return false;
    }

    /**
     * Lists all resources in a folder. This works for resources in a JAR or on disk
     * @param path the path with the last '/' removed
     * @return An alphabetically ordered list of resources.
     * @throws URISyntaxException When an incorrect URI format is used
     * @throws IOException When reading goes wrong.
     */
    private static List<String> listResources(String path) throws URISyntaxException, IOException {

        List<String> filenames = new ArrayList<>();

        URI uri = ZigNativeUtils.class.getResource(path).toURI();

        if (uri.getScheme().equals("jar")) {
            //List the files in the directory for JAR files
            Path myPath;
            FileSystem fileSystem = FileSystems.newFileSystem(uri, Collections.<String, Object>emptyMap());
            myPath = fileSystem.getPath(path);
            Stream<Path> walk = Files.walk(myPath, 1);
            for (Iterator<Path> it = walk.iterator(); it.hasNext();){
                filenames.add(it.next().toString());
            }
        } else {
            //List the files in the resources folder in the IDE / gradle
            try (InputStream in = new ZigNativeUtils().getClass().getResourceAsStream(path);
                 BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
                String resource;
                while ((resource = br.readLine()) != null) {
                    filenames.add(path +  File.separator + resource);
                }
            }
        }
        Collections.sort(filenames);
        return filenames;
    }



    /**
     * Loads library from current JAR archive
     * 
     * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after
     * exiting.
     * Method uses String as filename because the pathname is "abstract", not system-dependent.
     * 
     * @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext
     * @throws IOException If temporary file creation or read/write operation fails
     * @throws IllegalArgumentException If source file (param path) does not exist
     * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters
     * (restriction of {@link File#createTempFile(java.lang.String, java.lang.String)}).
     * @throws FileNotFoundException If the file could not be found inside the JAR.
     */
    public static void loadLibraryFromJar(String path) throws IOException {
 
        if (null == path || !path.startsWith("/")) {
            throw new IllegalArgumentException("The path has to be absolute (start with '/').");
        }
 
        // Obtain filename from path
        String[] parts = path.split("/");
        String filename = (parts.length > 1) ? parts[parts.length - 1] : null;
 
        // Check if the filename is okay
        if (filename == null || filename.length() < MIN_PREFIX_LENGTH) {
            throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
        }
 
        // Prepare temporary file
        if (temporaryDir == null) {
            temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX);
            temporaryDir.deleteOnExit();
        }

        File temp = new File(temporaryDir, filename);

        try (InputStream is = ZigNativeUtils.class.getResourceAsStream(path)) {
            Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
            System.err.println("Copied library to " + temp.getAbsolutePath() + " delete after use.");
        } catch (IOException e) {
            temp.delete();
            throw e;
        } catch (NullPointerException e) {
            temp.delete();
            throw new FileNotFoundException("File " + path + " was not found inside JAR.");
        }

        try {
            System.load(temp.getAbsolutePath());
        } finally {
            if (isPosixCompliant()) {
                // Assume POSIX compliant file system, can be deleted after loading
                temp.delete();
            } else {
                // Assume non-POSIX, and don't delete until last file descriptor closed
                temp.deleteOnExit();
            }
        }
    }

    private static boolean isPosixCompliant() {
        try {
            return FileSystems.getDefault()
                    .supportedFileAttributeViews()
                    .contains("posix");
        } catch (FileSystemNotFoundException
                | ProviderNotFoundException
                | SecurityException e) {
            return false;
        }
    }

    private static File createTempDirectory(String prefix) throws IOException {
        String tempDir = System.getProperty("java.io.tmpdir");
        File generatedDir = new File(tempDir, prefix + System.nanoTime());
        
        if (!generatedDir.mkdir())
            throw new IOException("Failed to create temp directory " + generatedDir.getName());
        
        return generatedDir;
    }

}